package middlewares

import (
	"fmt"
	"net/http"

	"gitee.com/liuxiaowei123/vblog-01/vblog-01/vblog/exception"
	"gitee.com/liuxiaowei123/vblog-01/vblog-01/vblog/ioc"
	"gitee.com/liuxiaowei123/vblog-01/vblog-01/vblog/response"
	"gitee.com/liuxiaowei123/vblog-01/vblog-01/vblog/token"
	"gitee.com/liuxiaowei123/vblog-01/vblog-01/vblog/user"
	"github.com/gin-gonic/gin"
)

// 此文件：基于gin中间件，对token校验

func NewTokenAuther() *TokenAuther {
	return &TokenAuther{
		tksvc: ioc.Controller().Get(token.Appname).(token.Service),
	}
}

// 此结构体中的属性，如何定义？
// 因为要对token做校验，调用ValiateToken，所以要有token.Service字段
type TokenAuther struct {
	tksvc token.Service
	// 为鉴权时使用，要判断上下文中的role和中间件中传入的role
	role user.Role
}

// Gin中间件 需要实现func(*Context)
func (t *TokenAuther) Auth(c *gin.Context) {
	// 1.获取token(从cookie中获取)
	cookietk, err := c.Cookie(token.TOKEN_COOKIE_NAME)
	if err != nil {
		// 判断没有获取到cookie时的报错
		// 没获取到cookie时，报错："Message": "http: named cookie not present"
		if err == http.ErrNoCookie {
			response.Failed(c, exception.NewAuthFailed("cookie %s not found", token.TOKEN_COOKIE_NAME))
			return
		}
		response.Failed(c, err)
		// return语句的作用是终止当前处理程序的执行并返回给调用者,如果不加上return语句，将会继续执行后续的处理程序
		return
	}

	// 2.调用token模块中的valiatetoken来认证
	valitetk := token.NewValiateTokenRequest(cookietk)
	// 错误代码：tk,err := t.tksvc.ValiateToken(c, valitetk)
	/* c 是指向 gin.Context 的指针，它包含了请求的信息、参数、中间件等，但并不一定包含上下文信息。而且，
	ValiateToken 函数可能需要的上下文信息并不是直接包含在 gin.Context 中的；因此，通常在处理 HTTP 请求时，
	需要使用 c.Request.Context() 来获取上下文，并将其传递给需要处理请求的函数 */
	tk, err := t.tksvc.ValiateToken(c.Request.Context(), valitetk)
	if err != nil {
		response.Failed(c, err)
		return
	}

	// 3.把校验后的结果：tk，放到请求的上下文中，方便后面的业务使用
	// Keys is a key/value pair exclusively for the context of each request.
	/* 关于c.Keys：在Gin框架中，每次请求都会创建一个新的c对象（通常是一个*Context类型的指针），
	该对象包含了许多与请求相关的属性和方法。其中，c.Keys是一个用于存储请求特定数据的字段。
	通过c.Keys，开发人员可以在中间件或路由处理程序中设置键值对，并在后续的处理程序中访问这些值 */
	// 因为Keys是map类型，所以需要初始化c.Keys，否则会报空指针
	if c.Keys == nil {
		c.Keys = map[string]any{}
		// 在c.Keys中设置键值对，来存储tk
		c.Keys[token.TOKEN_GIN_KEY_NAME] = tk
	}
}

// 鉴权中间件
func (t *TokenAuther) Perm(c *gin.Context) {
	// 1.从认证后的上下文获取数据
	contexttk := c.Keys[token.TOKEN_GIN_KEY_NAME]
	if contexttk == nil {
		response.Failed(c, exception.NewPermissionDeny("token not an *token.Token"))
		return
	}
	// 此段代码的写法，导致后面无法调用tk，原因在于没写ok的判断
	// if tk,ok := contexttk.(*token.Token); !ok{
	// 	response.Failed(c, exception.NewPermissionDeny("token not an *token.Token"))
	// 	return
	// }

	// v, ok := tk.(token.Token) 错误代码，导致报错
	// 使用 tk.(*token.Token) 进行断言比使用 tk.(token.Token) 更安全，
	// 因为它确保了断言只在 tk 是一个指向 token.Token 的指针时成功
	tk, ok := contexttk.(*token.Token)
	if !ok {
		response.Failed(c, exception.NewPermissionDeny("token not an *token.Token"))
		return
	}

	// 此段代码就是为了输出日志
	fmt.Printf("user %s role %d \n", tk.UserName, tk.Role)

	// 2.判断上下文token中role和传入的role是否一致; 如何向鉴权中间件传入参数的？ 在下面的函数中有写
	// 要提前在user中的CreateUserRequest对象和token对象中增加role
	// 要提前实现认证过后，更改token中的role信息
	// 要提前在TokenAuther对象中，增加role字段

	if tk.Role == user.ROLE_ADMIN {
		// 如果是Admin则直接放行
		// return 表示立即返回，终止执行
		return
	}

	// t.role就是传入的参数
	if tk.Role != t.role {
		response.Failed(c, exception.NewPermissionDeny("role %d not allow", tk.Role))
		return
	}
}

// 解决如何向鉴权中间件传入参数
// 携带参数的Gin中间件，在blog的路由中有调用此函数
func Required(r user.Role) gin.HandlerFunc {
	a := NewTokenAuther()
	a.role = r
	return a.Perm
}
